/**
 * 
 */
package network.internal.communication;

import static commons.Preconditions.checkNotNull;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import network.communication.Connection;
import network.communication.MessageHandler;
import network.internal.Message;
import server.AttributesStorer;
import server.StorageDevice;
import server.StorageUnitID;
import server.StorageUnitResolver;

import commons.FileAttributes;
import commons.exception.AlreadyUsedNameException;
import commons.exception.AttributeNotFoundException;
import commons.exception.ClosedConnectionException;
import commons.exception.NotFoundNameException;
import commons.exception.StorageUnitNotFoundException;
import commons.operation.Operation;
import commons.operation.OperationParameter;
import commons.operation.OperationType;

/**
 * @author armstrong
 * 
 */
public class ServerHandler implements MessageHandler {

	private final StorageUnitResolver	resolver;
	private final StorageDevice			storageDevice;
	private final AttributesStorer		attributesStorer;

	/**
	 * 
	 */
	public ServerHandler(final StorageUnitResolver resolver,
							final StorageDevice storageDevice,
							final AttributesStorer attributesStorer) {

		checkNotNull("resolver cannot be null.", resolver);
		checkNotNull("storageDevice cannot be null.", storageDevice);
		checkNotNull("attributesStorer cannot be null.", attributesStorer);

		this.resolver = resolver;
		this.storageDevice = storageDevice;
		this.attributesStorer = attributesStorer;
	}

	// FIXME I think this should check if the connection is opened.
	// Connection should throw a ClosedConnectionException or similar
	// if attempted to operate on a closed connection.
	/*
	 * (non-Javadoc)
	 * 
	 * @see network.MessageHandler#handle(java.net.Socket)
	 */
	@Override
	public void handle(final Connection connection) throws IOException,
			ClosedConnectionException {

		final Byte operationByte = connection.readByte();
		final Message message = Message.values()[operationByte];
		Exception thrownException = null;

		try {
			switch (message) {
				case CreateFile:
					handleCreate(connection);
					break;
				case DeleteFile:
					handleDelete(connection);
					break;
				case List:
					handleList(connection);
					break;
				case Size:
					handleSize(connection);
					break;
				case SetAttribute:
					handleSetAttribute(connection);
					break;
				case GetAttribute:
					handleGetAttribute(connection);
					break;
				case SetSize:
					handleSetSize(connection);
					break;
				case Write:
					handleWrite(connection);
					break;
				case Read:
					handleRead(connection);
					break;
				default:
					throw new UnsupportedOperationException();
			}
		} catch (final Exception e) {
			thrownException = e;
		}

		handleException(thrownException, connection);
	}

	/**
	 * @param connection
	 * @throws IOException
	 * @throws AlreadyUsedNameException
	 * @throws ClosedConnectionException
	 */
	private void handleCreate(final Connection connection) throws IOException,
			AlreadyUsedNameException, ClosedConnectionException {

		final String name = connection.readString();
		final StorageUnitID id = storageDevice.createStorageUnit();
		resolver.map(name, id);
		connection.writeByte((byte) Message.StatusOK.ordinal());
	}

	/**
	 * @param connection
	 * @throws IOException
	 * @throws NotFoundNameException
	 * @throws StorageUnitNotFoundException
	 * @throws ClosedConnectionException
	 */
	private void handleDelete(final Connection connection) throws IOException,
			NotFoundNameException, StorageUnitNotFoundException,
			ClosedConnectionException {

		final String name = connection.readString();

		final StorageUnitID id = resolver.resolve(name);
		resolver.delete(name);
		storageDevice.removeStorageUnit(id);
		connection.writeByte((byte) Message.StatusOK.ordinal());
	}

	private void handleException(final Exception e, final Connection connection)
			throws IOException, ClosedConnectionException {

		if (e != null) {
			if (e instanceof AlreadyUsedNameException) {
				connection.writeByte((byte) Message.FileAlreadyExist.ordinal());
			} else if (e instanceof NotFoundNameException) {
				connection.writeByte((byte) Message.FileDoesNotExist.ordinal());
			} else if (e instanceof StorageUnitNotFoundException) {
				// FIXME Think on what to do in this case.
				e.printStackTrace();
			} else if (e instanceof AttributeNotFoundException) {
				connection.writeByte((byte) Message.AttributeDoesNotExist
						.ordinal());
			}
		}
	}

	private void handleGetAttribute(final Connection connection)
			throws IOException, NotFoundNameException,
			AttributeNotFoundException, ClosedConnectionException {

		final String fileName = connection.readString();
		final String attributeName = connection.readString();

		final FileAttributes fileAttributes = attributesStorer
				.resolve(fileName);
		final String attributeValue = fileAttributes
				.getAttribute(attributeName);

		connection.writeByte((byte) Message.StatusOK.ordinal());
		connection.writeString(attributeValue);
	}

	/**
	 * @param connection
	 * @throws IOException
	 * @throws ClosedConnectionException
	 */
	private void handleList(final Connection connection) throws IOException,
			ClosedConnectionException {
		final List<String> filesList = resolver.list();

		connection.writeByte((byte) Message.StatusOK.ordinal());
		connection.writeString(((Integer) filesList.size()).toString());

		for (final String fileName : filesList) {
			connection.writeString(fileName);
		}
	}

	private void handleRead(final Connection connection) throws IOException,
			NotFoundNameException, StorageUnitNotFoundException,
			ClosedConnectionException {

		final String fileName = connection.readString();

		// FIXME It's possible that a exception be thrown here
		// FIXME What if one of these numbers is negative?
		final Integer readSize = new Integer(connection.readString());
		final Long position = new Long(connection.readString());

		final StorageUnitID storageID = resolver.resolve(fileName);

		final Map<OperationParameter, Object> parametersSeek = new HashMap<OperationParameter, Object>();
		parametersSeek.put(OperationParameter.Position, position);
		final Operation seek = new Operation(OperationType.Seek, parametersSeek);

		storageDevice.operateOverStorageUnit(seek, storageID);

		// FIXME I think this method should not allocate the entire space to
		// read
		// it should read small blocks and then write them on at a time
		final ByteBuffer buffer = ByteBuffer.allocate(readSize);

		final Map<OperationParameter, Object> parametersRead = new HashMap<OperationParameter, Object>();
		parametersRead.put(OperationParameter.ByteBuffer, buffer);
		final Operation readOperation = new Operation(OperationType.Read,
				parametersRead);

		final Integer readBytes = (Integer) storageDevice
				.operateOverStorageUnit(readOperation, storageID);

		// reseting the buffer to be written to the client
		buffer.position(0);

		connection.writeByte((byte) Message.StatusOK.ordinal());
		connection.writeString(readBytes.toString());
		connection.writeByteBuffer(buffer);
	}

	/**
	 * @param connection
	 * @throws IOException
	 * @throws NotFoundNameException
	 * @throws ClosedConnectionException
	 */
	private void handleSetAttribute(final Connection connection)
			throws IOException, NotFoundNameException,
			ClosedConnectionException {

		final String fileName = connection.readString();
		final String attributeName = connection.readString();
		final String attributeValue = connection.readString();

		final FileAttributes attributes = attributesStorer.resolve(fileName);

		attributes.setAttribute(attributeName, attributeValue);
		connection.writeByte((byte) Message.StatusOK.ordinal());
	}

	private void handleSetSize(final Connection connection) throws IOException,
			NotFoundNameException, StorageUnitNotFoundException,
			ClosedConnectionException {

		final String fileName = connection.readString();
		final String sizeString = connection.readString();

		// FIXME It's possible that a exception be thrown here
		// FIXME What if newSize is negative?
		final Long newSize = new Long(sizeString);

		final StorageUnitID storageID = resolver.resolve(fileName);

		final Map<OperationParameter, Object> parametersSetSize = new HashMap<OperationParameter, Object>();
		parametersSetSize.put(OperationParameter.Size, newSize);
		final Operation setSize = new Operation(OperationType.SetSize,
				parametersSetSize);

		storageDevice.operateOverStorageUnit(setSize, storageID);

		connection.writeByte((byte) Message.StatusOK.ordinal());
	}

	/**
	 * @param connection
	 * @throws IOException
	 * @throws NotFoundNameException
	 * @throws StorageUnitNotFoundException
	 * @throws ClosedConnectionException
	 */
	private void handleSize(final Connection connection) throws IOException,
			NotFoundNameException, StorageUnitNotFoundException,
			ClosedConnectionException {

		final String name = connection.readString();
		StorageUnitID storageID = null;
		storageID = resolver.resolve(name);

		final Map<OperationParameter, Object> parametersSize = new HashMap<OperationParameter, Object>();
		final Operation size = new Operation(OperationType.Size, parametersSize);
		final Long sizeResult = (Long) storageDevice.operateOverStorageUnit(
				size, storageID);

		connection.writeByte((byte) Message.StatusOK.ordinal());
		connection.writeString(sizeResult.toString());
	}

	private void handleWrite(final Connection connection) throws IOException,
			NotFoundNameException, StorageUnitNotFoundException,
			ClosedConnectionException {

		final String fileName = connection.readString();

		// FIXME It's possible that a exception be thrown here
		// FIXME What if one of these numbers is negative?
		// FIXME I think these should be Integers
		final Long writeSize = new Long(connection.readString());
		final Long position = new Long(connection.readString());

		final StorageUnitID storageID = resolver.resolve(fileName);

		Long totalWritten = 0L;

		final Map<OperationParameter, Object> parametersSeek = new HashMap<OperationParameter, Object>();
		parametersSeek.put(OperationParameter.Position, position);
		final Operation seek = new Operation(OperationType.Seek, parametersSeek);

		storageDevice.operateOverStorageUnit(seek, storageID);

		ByteBuffer buffer = null;

		// FIXME possible infinite loop?
		while (totalWritten < writeSize) {
			buffer = connection.readByteBuffer();

			final Map<OperationParameter, Object> parametersWrite = new HashMap<OperationParameter, Object>();
			parametersWrite.put(OperationParameter.ByteBuffer, buffer);
			final Operation write = new Operation(OperationType.Write,
					parametersWrite);

			final Integer written = (Integer) storageDevice
					.operateOverStorageUnit(write, storageID);
			totalWritten += written;
		}

		connection.writeByte((byte) Message.StatusOK.ordinal());
		connection.writeString(totalWritten.toString());
	}
}
